/** 
 * AJAX Upload ( http://valums.com/ajax-upload/ )  
 * Copyright (c) Andris Valums 
 * Licensed under the MIT license ( http://valums.com/mit-license/ ) 
 * Thanks to Gary Haran, David Mark, Corey Burns and others for contributions  
 */  
(function () {  
    /* global window */  
    /* jslint browser: true, devel: true, undef: true, nomen: true, bitwise: true, regexp: true, newcap: true, immed: true */  
  
    /** 
     * Wrapper for FireBug's console.log 
     */  
  
    function log() {  
        if (typeof(console) != 'undefined' && typeof(console.log) == 'function') {  
            Array.prototype.unshift.call(arguments, '[Ajax Upload]');  
            console.log(Array.prototype.join.call(arguments, ' '));  
        }  
    }  
  
    /** 
     * Attaches event to a dom element. 
     * @param {Element} el 
     * @param type event name 
     * @param fn callback This refers to the passed element 
     */  
  
    function addEvent(el, type, fn) {  
        if (el.addEventListener) {  
            el.addEventListener(type, fn, false);  
        } else if (el.attachEvent) {  
            el.attachEvent('on' + type, function () {  
                fn.call(el);  
            });  
        } else {  
            throw new Error('not supported or DOM not loaded');  
        }  
    }  
  
    /** 
     * Attaches resize event to a window, limiting 
     * number of event fired. Fires only when encounteres 
     * delay of 100 after series of events. 
     *  
     * Some browsers fire event multiple times when resizing 
     * http://www.quirksmode.org/dom/events/resize.html 
     *  
     * @param fn callback This refers to the passed element 
     */  
  
    function addResizeEvent(fn) {  
        var timeout;  
  
        addEvent(window, 'resize', function () {  
            if (timeout) {  
                clearTimeout(timeout);  
            }  
            timeout = setTimeout(fn, 100);  
        });  
    }  
  
    // Needs more testing, will be rewriten for next version          
    // getOffset function copied from jQuery lib (http://jquery.com/)  
    if (document.documentElement.getBoundingClientRect) {  
        // Get Offset using getBoundingClientRect  
        // http://ejohn.org/blog/getboundingclientrect-is-awesome/  
        var getOffset = function (el) {  
            var box = el.getBoundingClientRect();  
            var doc = el.ownerDocument;  
            var body = doc.body;  
            var docElem = doc.documentElement; // for ie   
            var clientTop = docElem.clientTop || body.clientTop || 0;  
            var clientLeft = docElem.clientLeft || body.clientLeft || 0;  
  
            // In Internet Explorer 7 getBoundingClientRect property is treated as physical,  
            // while others are logical. Make all logical, like in IE8.   
            var zoom = 1;  
            if (body.getBoundingClientRect) {  
                var bound = body.getBoundingClientRect();  
                zoom = (bound.right - bound.left) / body.clientWidth;  
            }  
  
            if (zoom > 1) {  
                clientTop = 0;  
                clientLeft = 0;  
            }  
  
            var top = box.top / zoom + (window.pageYOffset || docElem && docElem.scrollTop / zoom || body.scrollTop / zoom) - clientTop,  
                left = box.left / zoom + (window.pageXOffset || docElem && docElem.scrollLeft / zoom || body.scrollLeft / zoom) - clientLeft;  
  
            return {  
                top: top,  
                left: left  
            };  
        };  
    } else {  
        // Get offset adding all offsets   
        var getOffset = function (el) {  
            var top = 0,  
                left = 0;  
            do {  
                top += el.offsetTop || 0;  
                left += el.offsetLeft || 0;  
                el = el.offsetParent;  
            } while (el);  
  
            return {  
                left: left,  
                top: top  
            };  
        };  
    }  
  
    /** 
     * Returns left, top, right and bottom properties describing the border-box, 
     * in pixels, with the top-left relative to the body 
     * @param {Element} el 
     * @return {Object} Contains left, top, right,bottom 
     */  
  
    function getBox(el) {  
        var left, right, top, bottom;  
        var offset = getOffset(el);  
        left = offset.left;  
        top = offset.top;  
  
        right = left + el.offsetWidth;  
        bottom = top + el.offsetHeight;  
  
        return {  
            left: left,  
            right: right,  
            top: top,  
            bottom: bottom  
        };  
    }  
  
    /** 
     * Helper that takes object literal 
     * and add all properties to element.style 
     * @param {Element} el 
     * @param {Object} styles 
     */  
  
    function addStyles(el, styles) {  
        for (var name in styles) {  
            if (styles.hasOwnProperty(name)) {  
                el.style[name] = styles[name];  
            }  
        }  
    }  
  
    /** 
     * Function places an absolutely positioned 
     * element on top of the specified element 
     * copying position and dimentions. 
     * @param {Element} from 
     * @param {Element} to 
     */  
  
    function copyLayout(from, to) {  
        var box = getBox(from);  
  
        addStyles(to, {  
            position: 'absolute',  
            left: box.left + 'px',  
            top: box.top + 'px',  
            width: from.offsetWidth + 'px',  
            height: from.offsetHeight + 'px'  
        });  
    }  
  
    /** 
     * Creates and returns element from html chunk 
     * Uses innerHTML to create an element 
     */  
    var toElement = (function () {  
        var div = document.createElement('div');  
        return function (html) {  
            div.innerHTML = html;  
            var el = div.firstChild;  
            return div.removeChild(el);  
        };  
    })();  
  
    /** 
     * Function generates unique id 
     * @return unique id  
     */  
    var getUID = (function () {  
        var id = 0;  
        return function () {  
            return 'ValumsAjaxUpload' + id++;  
        };  
    })();  
  
    /** 
     * Get file name from path 
     * @param {String} file path to file 
     * @return filename 
     */  
  
    function fileFromPath(file) {  
        return file.replace(/.*(\/|\\)/, "");  
    }  
  
    /** 
     * Get file extension lowercase 
     * @param {String} file name 
     * @return file extenstion 
     */  
  
    function getExt(file) {  
        return (-1 !== file.indexOf('.')) ? file.replace(/.*[.]/, '') : '';  
    }  
  
    function hasClass(el, name) {  
        var re = new RegExp('\\b' + name + '\\b');  
        return re.test(el.className);  
    }  
  
    function addClass(el, name) {  
        if (!hasClass(el, name)) {  
            el.className += ' ' + name;  
        }  
    }  
  
    function removeClass(el, name) {  
        var re = new RegExp('\\b' + name + '\\b');  
        el.className = el.className.replace(re, '');  
    }  
  
    function removeNode(el) {  
        el.parentNode.removeChild(el);  
    }  
  
    /** 
     * Easy styling and uploading 
     * @constructor 
     * @param button An element you want convert to  
     * upload button. Tested dimentions up to 500x500px 
     * @param {Object} options See defaults below. 
     */  
    window.AjaxUpload = function (button, options) {  
        this._settings = {  
            // Location of the server-side upload script  
            action: 'upload.php',  
            // File upload name  
            name: 'userfile',  
            // Additional data to send  
            data: {},  
            // Submit file as soon as it's selected  
            autoSubmit: true,  
            // The type of data that you're expecting back from the server.  
            // html and xml are detected automatically.  
            // Only useful when you are using json data as a response.  
            // Set to "json" in that case.   
            responseType: false,  
            // Class applied to button when mouse is hovered  
            hoverClass: 'hover',  
            // Class applied to button when AU is disabled  
            disabledClass: 'disabled',  
            // When user selects a file, useful with autoSubmit disabled  
            // You can return false to cancel upload              
            onChange: function (file, extension) {},  
            // Callback to fire before file is uploaded  
            // You can return false to cancel upload  
            onSubmit: function (file, extension) {},  
            // Fired when file upload is completed  
            // WARNING! DO NOT USE "FALSE" STRING AS A RESPONSE!  
            onComplete: function (file, response) {}  
        };  
  
        // Merge the users options with our defaults  
        for (var i in options) {  
            if (options.hasOwnProperty(i)) {  
                this._settings[i] = options[i];  
            }  
        }  
  
        // button isn't necessary a dom element  
        if (button.jquery) {  
            // jQuery object was passed  
            button = button[0];  
        } else if (typeof button == "string") {  
            if (/^#.*/.test(button)) {  
                // If jQuery user passes #elementId don't break it                    
                button = button.slice(1);  
            }  
  
            button = document.getElementById(button);  
        }  
  
        if (!button || button.nodeType !== 1) {  
            throw new Error("Please make sure that you're passing a valid element");  
        }  
  
        if (button.nodeName.toUpperCase() == 'A') {  
            // disable link                         
            addEvent(button, 'click', function (e) {  
                if (e && e.preventDefault) {  
                    e.preventDefault();  
                } else if (window.event) {  
                    window.event.returnValue = false;  
                }  
            });  
        }  
  
        // DOM element  
        this._button = button;  
        // DOM element                   
        this._input = null;  
        // If disabled clicking on button won't do anything  
        this._disabled = false;  
  
        // if the button was disabled before refresh if will remain  
        // disabled in FireFox, let's fix it  
        this.enable();  
  
        this._rerouteClicks();  
    };  
  
    // assigning methods to our class  
    AjaxUpload.prototype = {  
        setData: function (data) {  
            this._settings.data = data;  
        },  
        disable: function () {  
            addClass(this._button, this._settings.disabledClass);  
            this._disabled = true;  
  
            var nodeName = this._button.nodeName.toUpperCase();  
            if (nodeName == 'INPUT' || nodeName == 'BUTTON') {  
                this._button.setAttribute('disabled', 'disabled');  
            }  
  
            // hide input  
            if (this._input) {  
                // We use visibility instead of display to fix problem with Safari 4  
                // The problem is that the value of input doesn't change if it   
                // has display none when user selects a file             
                this._input.parentNode.style.visibility = 'hidden';  
            }  
        },  
        enable: function () {  
            removeClass(this._button, this._settings.disabledClass);  
            this._button.removeAttribute('disabled');  
            this._disabled = false;  
  
        },  
        /** 
         * Creates invisible file input  
         * that will hover above the button 
         * <div><input type='file' /></div> 
         */  
        _createInput: function () {  
            var self = this;  
  
            var input = document.createElement("input");  
            input.setAttribute('type', 'file');  
            input.setAttribute('name', this._settings.name);  
  
            addStyles(input, {  
                'position': 'absolute',  
                // in Opera only 'browse' button  
                // is clickable and it is located at  
                // the right side of the input  
                'right': 0,  
                'margin': 0,  
                'padding': 0,  
                'fontSize': '480px',  
                'cursor': 'pointer'  
            });  
  
            var div = document.createElement("div");  
            addStyles(div, {  
                'display': 'block',  
                'position': 'absolute',  
                'overflow': 'hidden',  
                'margin': 0,  
                'padding': 0,  
                'opacity': 0,  
                // Make sure browse button is in the right side  
                // in Internet Explorer  
                'direction': 'ltr',  
                //Max zIndex supported by Opera 9.0-9.2  
                'zIndex': 2147483583  
            });  
  
            // Make sure that element opacity exists.  
            // Otherwise use IE filter              
            if (div.style.opacity !== "0") {  
                if (typeof(div.filters) == 'undefined') {  
                    throw new Error('Opacity not supported by the browser');  
                }  
                div.style.filter = "alpha(opacity=0)";  
            }  
  
            addEvent(input, 'change', function () {  
  
                if (!input || input.value === '') {  
                    return;  
                }  
  
                // Get filename from input, required                  
                // as some browsers have path instead of it            
                var file = fileFromPath(input.value);  
  
                if (false === self._settings.onChange.call(self, file, getExt(file))) {  
                    self._clearInput();  
                    return;  
                }  
  
                // Submit form when value is changed  
                if (self._settings.autoSubmit) {  
                    self.submit();  
                }  
            });  
  
            addEvent(input, 'mouseover', function () {  
                addClass(self._button, self._settings.hoverClass);  
            });  
  
            addEvent(input, 'mouseout', function () {  
                removeClass(self._button, self._settings.hoverClass);  
  
                // We use visibility instead of display to fix problem with Safari 4  
                // The problem is that the value of input doesn't change if it   
                // has display none when user selects a file             
                input.parentNode.style.visibility = 'hidden';  
  
            });  
  
            div.appendChild(input);  
            document.body.appendChild(div);  
  
            this._input = input;  
        },  
        _clearInput: function () {  
            if (!this._input) {  
                return;  
            }  
  
            // this._input.value = ''; Doesn't work in IE6                                 
            removeNode(this._input.parentNode);  
            this._input = null;  
            this._createInput();  
  
            removeClass(this._button, this._settings.hoverClass);  
        },  
        /** 
         * Function makes sure that when user clicks upload button, 
         * the this._input is clicked instead 
         */  
        _rerouteClicks: function () {  
            var self = this;  
  
            // IE will later display 'access denied' error  
            // if you use using self._input.click()  
            // other browsers just ignore click()  
  
            addEvent(self._button, 'mouseover', function () {  
                if (self._disabled) {  
                    return;  
                }  
  
                if (!self._input) {  
                    self._createInput();  
                }  
  
                var div = self._input.parentNode;  
                copyLayout(self._button, div);  
                div.style.visibility = 'visible';  
  
            });  
  
  
            // commented because we now hide input on mouseleave  
            /** 
             * When the window is resized the elements  
             * can be misaligned if button position depends 
             * on window size 
             */  
            //addResizeEvent(function(){  
            //    if (self._input){  
            //        copyLayout(self._button, self._input.parentNode);  
            //    }  
            //});              
  
        },  
        /** 
         * Creates iframe with unique name 
         * @return {Element} iframe 
         */  
        _createIframe: function () {  
            // We can't use getTime, because it sometimes return  
            // same value in safari :(  
            var id = getUID();  
  
            // We can't use following code as the name attribute  
            // won't be properly registered in IE6, and new window  
            // on form submit will open  
            // var iframe = document.createElement('iframe');  
            // iframe.setAttribute('name', id);                          
  
            var iframe = toElement('<iframe src="javascript:false;" name="' + id + '" />');  
            // src="javascript:false; was added  
            // because it possibly removes ie6 prompt   
            // "This page contains both secure and nonsecure items"  
            // Anyway, it doesn't do any harm.              
            iframe.setAttribute('id', id);  
  
            iframe.style.display = 'none';  
            document.body.appendChild(iframe);  
  
            return iframe;  
        },  
        /** 
         * Creates form, that will be submitted to iframe 
         * @param {Element} iframe Where to submit 
         * @return {Element} form 
         */  
        _createForm: function (iframe) {  
            var settings = this._settings;  
  
            // We can't use the following code in IE6  
            // var form = document.createElement('form');  
            // form.setAttribute('method', 'post');  
            // form.setAttribute('enctype', 'multipart/form-data');  
            // Because in this case file won't be attached to request                      
            var form = toElement('<form method="post" enctype="multipart/form-data"></form>');  
  
            form.setAttribute('action', settings.action);  
            form.setAttribute('target', iframe.name);  
            form.style.display = 'none';  
            document.body.appendChild(form);  
  
            // Create hidden input element for each data key  
            for (var prop in settings.data) {  
                if (settings.data.hasOwnProperty(prop)) {  
                    var el = document.createElement("input");  
                    el.setAttribute('type', 'hidden');  
                    el.setAttribute('name', prop);  
                    el.setAttribute('value', settings.data[prop]);  
                    form.appendChild(el);  
                }  
            }  
            return form;  
        },  
        /** 
         * Gets response from iframe and fires onComplete event when ready 
         * @param iframe 
         * @param file Filename to use in onComplete callback  
         */  
        _getResponse: function (iframe, file) {  
            // getting response  
            var toDeleteFlag = false,  
                self = this,  
                settings = this._settings;  
  
            addEvent(iframe, 'load', function () {  
  
                if ( // For Safari   
                iframe.src == "javascript:'%3Chtml%3E%3C/html%3E';" ||  
                // For FF, IE  
                iframe.src == "javascript:'<html></html>';") {  
                    // First time around, do not delete.  
                    // We reload to blank page, so that reloading main page  
                    // does not re-submit the post.  
  
                    if (toDeleteFlag) {  
                        // Fix busy state in FF3  
                        setTimeout(function () {  
                            removeNode(iframe);  
                        },  
                        0);  
                    }  
  
                    return;  
                }  
  
                var doc = iframe.contentDocument ? iframe.contentDocument : window.frames[iframe.id].document;  
  
                // fixing Opera 9.26,10.00  
                if (doc.readyState && doc.readyState != 'complete') {  
                    // Opera fires load event multiple times  
                    // Even when the DOM is not ready yet  
                    // this fix should not affect other browsers  
                    return;  
                }  
  
                // fixing Opera 9.64  
                if (doc.body && doc.body.innerHTML == "false") {  
                    // In Opera 9.64 event was fired second time  
                    // when body.innerHTML changed from false   
                    // to server response approx. after 1 sec  
                    return;  
                }  
  
                var response;  
  
                if (doc.XMLDocument) {  
                    // response is a xml document Internet Explorer property  
                    response = doc.XMLDocument;  
                } else if (doc.body) {  
                    // response is html document or plain text  
                    response = doc.body.innerHTML;  
  
                    if (settings.responseType && settings.responseType.toLowerCase() == 'json') {  
                        // If the document was sent as 'application/javascript' or  
                        // 'text/javascript', then the browser wraps the text in a <pre>  
                        // tag and performs html encoding on the contents.  In this case,  
                        // we need to pull the original text content from the text node's  
                        // nodeValue property to retrieve the unmangled content.  
                        // Note that IE6 only understands text/html  
                        if (doc.body.firstChild && doc.body.firstChild.nodeName.toUpperCase() == 'PRE') {  
                            response = doc.body.firstChild.firstChild.nodeValue;  
                        }  
  
                        if (response) {  
                            response = eval("(" + response + ")");  
                        } else {  
                            response = {};  
                        }  
                    }  
                } else {  
                    // response is a xml document  
                    response = doc;  
                }  
  
                settings.onComplete.call(self, file, response);  
  
                // Reload blank page, so that reloading main page  
                // does not re-submit the post. Also, remember to  
                // delete the frame  
                toDeleteFlag = true;  
  
                // Fix IE mixed content issue  
                iframe.src = "javascript:'<html></html>';";  
            });  
        },  
        /** 
         * Upload file contained in this._input 
         */  
        submit: function () {  
            var self = this,  
                settings = this._settings;  
  
            if (!this._input || this._input.value === '') {  
                return;  
            }  
  
            var file = fileFromPath(this._input.value);  
  
            // user returned false to cancel upload  
            if (false === settings.onSubmit.call(this, file, getExt(file))) {  
                this._clearInput();  
                return;  
            }  
  
            // sending request      
            var iframe = this._createIframe();  
            var form = this._createForm(iframe);  
  
            // assuming following structure  
            // div -> input type='file'  
            removeNode(this._input.parentNode);  
            removeClass(self._button, self._settings.hoverClass);  
  
            form.appendChild(this._input);  
  
            form.submit();  
  
            // request set, clean up                  
            removeNode(form);  
            form = null;  
            removeNode(this._input);  
            this._input = null;  
  
            // Get response from iframe and fire onComplete event when ready  
            this._getResponse(iframe, file);  
  
            // get ready for next request              
            this._createInput();  
        }  
    };  
})();  